iT邦幫忙

2022 iThome 鐵人賽

DAY 9
0
Software Development

《今天也走在開發遊戲引擎的路上》系列 第 9

「遊戲開發知識科普」 —— ECS 架構

  • 分享至 

  • xImage
  •  


(圖/Entity Component System for Unity: Getting Started)

ECS架構,也就是Entity-Componet-System。由「實體」、「組件」、「系統」三個單字所組成的一種程式架構,有別於傳統物件導向OOP(Object Oriented Programming)的概念,ECS是建立在資料導向DOP(Data Oriented Programming)的概念上。這裡稍微簡單介紹一下關於OOP與DOP。

OOP

可以理解成以我們的視角來對一個概念去做抽象,定義成一個類(Class),包含屬性(Field)、方法(Method)。而我們在實作時需要有物件(Object),也就是Class的實例,而物件與物件間互相不影響,皆為獨立的。至於其他特性這裡就先不說明了。

而OOP在大型的遊戲設計中並非最佳的選擇。

  • OOP的物件往往是龐大的,若你只是要訪問幾個物件裡的屬性資料,但卻要把整個物件載入快取,這對CPU來說是一種浪費。
  • 當你需要對某個屬性做操作時,每個屬性的資料分散在物件之中,管理上不方便。

DOP

而DOP則是以另外一種方式來思考,假設你有N個物件,每個物件有M種屬性,將個別的屬性用一個array來儲存,這麼設計的話會有什麼作用呢?

  • 對快取十分友善,當我們要對某個特定屬性資料做操作時,由於陣列的特性,資料在記憶體空間裡是連續的。

ECS

因此我們就來介紹一下ECS架構具體一點的邏輯。

  • 實體(Entity)
    是一種概念,在ECS架構裡沒有物件的概念,而是以實體稱乎,實體是組件的集合,而通常時體會以ID來進行表示。舉例來說,一個玩家可以是實體,而他會由位置、血量、模型等等組件所組成,而實體應該要能隨時新增、刪除組件。

    • Player(Position,Health,Model)
  • 組件(Componet)
    組件則是一種資料的集合,資料會代表一種實體的特性,就像是「位置」、「血量」等等...由於實體可以新增、刪除組件,我們也能藉由組件來對實體做狀態的標記等等,例如玩家中毒時可以新增一個中毒的組件。

  • 系統(System)
    系統則是在迴圈中負責去對組件進行操作的概念。一個系統只有方法而沒有屬性,舉例來說,移動系統就是一個負責更新實體位置的系統,控制擁有位置組件與速度組件的實體集合,並為他們計算、更新位置完成移動。

Entt

Entt是一個C++函式庫,雖然我們擁有了ECS的概念,但自行去編寫或許還是不比原先的方法來的有效率。而Entt就是一種ECS的框架,而Mojang 的 Minecraft就是使用Entt來作為他的ECS系統的。

而筆者前幾天介紹的vcpkg也支持Entt。

而我們也來簡單看看官方範例(這裡的代碼示例)。

可以先看到在最上面,範例先建立了兩個struct,position(位置)和velocity(速度),而他們各有兩個儲存資料。

struct position {
    float x;
    float y;
};

struct velocity {
    float dx;
    float dy;
};

讓我們看到main函式中,範例先宣告了一個用於管理的註冊表。

entt::registry registry;

接著跑迴圈,每個迴圈中都建立一個實體,並將實體加入位置屬性,而偶數位的實體則多加入速度屬性。並執行update函數。

for(auto i = 0u; i < 10u; ++i) {
        const auto entity = registry.create();
        registry.emplace<position>(entity, i * 1.f, i * 1.f);
        if(i % 2 == 0) { registry.emplace<velocity>(entity, i * .1f, i * .1f); }
    }

讓我們來看update的部分,這裡介紹了四種用來遍歷組件的方法。

void update(entt::registry &registry) {
    auto view = registry.view<const position, velocity>();

    // use a callback
    view.each([](const auto &pos, auto &vel) { /* ... */ });

    // use an extended callback
    view.each([](const auto entity, const auto &pos, auto &vel) { /* ... */ });

    // use a range-for
    for(auto [entity, pos, vel]: view.each()) {
        // ...
    }

    // use forward iterators and get only the components of interest
    for(auto entity: view) {
        auto &vel = view.get<velocity>(entity);
        // ...
    }
}

我們一個一個來拆解。首先先宣告了一個view,用來取得擁有position與velocity組件的實體。

auto view = registry.view<const position, velocity>();

第一種方法,可以透過each函式來遍歷。可以傳入一個lambda函式來控制每個實體在更新時要做的動作。

// use a callback 
view.each([](const auto &pos, auto &vel) { /* ... */ });

而第二種與第一種是相同的,差別在於操作時可以額外的對實體進行其他操作。

// use an extended callback
view.each([](const auto entity, const auto &pos, auto &vel) { /* ... */ });

第三、四種則是使用Range-based for Statement來操作,透過structured binding來遍歷view.each(),或是可以直接遍歷view取得實體,並使用get<組件>(實體) 來取得實體組件的值。

// use a range-for
for(auto [entity, pos, vel]: view.each()) {
    // ...
}

// use forward iterators and get only the components of interest
for(auto entity: view) {
    auto &vel = view.get<velocity>(entity);
    // ...
}

後記

總體而言,ECS架構理論上性能上是與傳統OOP的方法有所提升,不過筆者也還並未實作過,這裡僅以科普的角度來介紹一下這個架構,若有誤還請包涵與指出!
/images/emoticon/emoticon06.gif

題外話,今天晚上在打俄羅斯方塊的比賽。原本早上預計寫個幾篇,太晚睡醒加上花了一點時間暖身... 總而言之就先用科普文章來頂個一天...QAQ,(不幸的進了八強! 明天繼續比賽)


上一篇
「專案建立及管理」 —— vcpkg
下一篇
「中場休息」—— 閒聊
系列文
《今天也走在開發遊戲引擎的路上》12
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言